#!/usr/bin/env python
# -*- coding:utf8 -*-
# uncompyle6 version 2.15.1
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.10 (default, Jul  1 2017, 13:36:56) 
# [GCC 4.4.6 20110731 (Red Hat 4.4.6-4)]
# Embedded file name: ./paas/saas/utils.py
# Compiled at: 2017-11-16 15:44:28
"""
Tencent is pleased to support the open source community by making 蓝鲸智云(BlueKing) available.
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
"""
import os, tarfile, hashlib, json
from django.conf import settings
from django.db import transaction, connections
from django.shortcuts import HttpResponse
from app.models import App, SecureInfo
from common.log import logger
from common.utils import record_user_operate
from engine.api import register_app
from release.models import Record
from release.constants import UserOperateTypeEnum
from saas.models import SaaSApp, SaaSAppVersion
from saas.constants import UPLOAD_RESPONSE_FORMAT, CREATE_PAAS_DB_SQL, SAAS_APP_LOGO_IMG_RELATED

def md5_for_file(chunks):
    """
    计算文件的md5
    """
    md5 = hashlib.md5()
    for data in chunks:
        md5.update(data)

    return md5.hexdigest()


def validate_and_extract_tar_file(filename, path):
    """
    校验tar.gz文件, 并解压获取app.yml
    """
    try:
        tar_file = tarfile.open(path)
    except:
        message = u'%s 文件格式错误, 无法正常打开, 请确认后重新上传' % filename
        logger.exception(message)
        return (
         False, message, None)

    try:
        tar_file_members = tar_file.getmembers()
    except:
        message = u'%s 文件错误, 无法正常打开解析文件列表, 请确认后重新上传' % filename
        logger.exception(message)
        return (
         False, message, None)

    app_yml_path_list = [ m.name for m in tar_file_members if m.name.endswith('app.yml') and len(m.name.split('/')) == 2 ]
    if not app_yml_path_list:
        message = u'%s 包中缺少配置文件 app.yml' % filename
        logger.error(message)
        return (
         False, message, None)
    app_yml_path = app_yml_path_list[0]
    try:
        app_yml_file = tar_file.extractfile(app_yml_path)
        app_yml_content = app_yml_file.read()
    except:
        message = u'%s 解压获取 app.yml 失败' % filename
        logger.exception(message)
        return (
         False, message, None)

    try:
        tar_file.close()
    except:
        logger.exception()

    return (
     True, u'处理成功', app_yml_content)


def extract_logo_file(filename, path, saas_app_code):
    """
    解压tar.gz文件中的logo到 /media/saaslogo/
    """
    try:
        tar_file = tarfile.open(path)
    except:
        message = u'%s 文件格式错误, 无法正常打开, 请确认后重新上传' % filename
        logger.exception(message)
        return (
         False, message)

    try:
        tar_file_members = tar_file.getmembers()
    except:
        message = u'%s 文件错误, 无法正常打开解析文件列表, 请确认后重新上传' % filename
        logger.exception(message)
        return (
         False, message)

    app_logo_name = '%s.png' % saas_app_code
    logo_path_list = [ m.name for m in tar_file_members if m.name.endswith(app_logo_name) and len(m.name.split('/')) == 3
                     ]
    if not logo_path_list:
        message = u'%s 包中缺少配置文件LOGO文件: %s' % (filename, app_logo_name)
        logger.error(message)
        return (
         False, message)
    logo_path = logo_path_list[0]
    try:
        logo_file = tar_file.extractfile(logo_path)
        logo_content = logo_file.read()
        logo_local_path = os.path.join(settings.MEDIA_ROOT, SAAS_APP_LOGO_IMG_RELATED, app_logo_name)
        applogo_path = os.path.join(settings.MEDIA_ROOT, 'applogo', app_logo_name)
        is_replace_applog = False
        if os.path.exists(logo_local_path) and os.path.exists(applogo_path):
            import filecmp
            if filecmp.cmp(logo_local_path, applogo_path):
                is_replace_applog = True
        with open(logo_local_path, 'w') as (logo_f):
            logo_f.write(logo_content)
        if not os.path.exists(applogo_path) or is_replace_applog:
            with open(applogo_path, 'w') as (logo_f):
                logo_f.write(logo_content)
    except:
        message = u'%s 解压获取 app.yml 失败' % filename
        logger.exception(message)
        return (
         False, message)

    try:
        tar_file.close()
    except:
        logger.exception()

    return (
     True, u'处理成功')


@transaction.atomic
def save_saas_app_info(app_config, saas_upload_file):
    app_code = app_config.get('app_code')
    app_name = app_config.get('app_name')
    version = app_config.get('version')
    language = app_config.get('language', 'python')
    is_use_celery = app_config.get('is_use_celery', True)
    author = app_config.get('author', '')
    introduction = app_config.get('introduction', '')
    category = app_config.get('category', '')
    language_support = app_config.get('language_support', '')
    desktop = app_config.get('desktop')
    date = app_config.get('date')
    try:
        if date:
            date = date.strftime('%Y-%m-%d %H:%M:%S')
    except:
        date = str(date)

    libraries = app_config.get('libraries', [])
    yums = app_config.get('yums', [])
    pip = (' ').join([ ('{name}=={version}').format(**d) for d in libraries ])
    yum = (' ').join(yums)
    settings = {'language': language,
       'is_use_celery': is_use_celery,
       'author': author,
       'introduction': introduction,
       'category': category,
       'language_support': language_support,
       'date': date,
       'pip': pip,
       'yum': yum,
       'desktop': desktop
       }
    is_saas_app_exist = False
    try:
        saas_app = SaaSApp.objects.get(code=app_code)
        saas_app.name = app_name
        is_saas_app_exist = True
    except SaaSApp.DoesNotExist:
        saas_app = SaaSApp()
        saas_app.code = app_code
        saas_app.name = app_name
        saas_app.save()

    if is_saas_app_exist:
        try:
            saas_app_version = SaaSAppVersion.objects.get(version=version, saas_app=saas_app)
        except SaaSAppVersion.DoesNotExist:
            saas_app_version = SaaSAppVersion()
            saas_app_version.version = version
            saas_app_version.saas_app = saas_app

    else:
        saas_app_version = SaaSAppVersion()
        saas_app_version.version = version
        saas_app_version.saas_app = saas_app
    saas_app_version.settings = json.dumps(settings)
    saas_app_version.upload_file = saas_upload_file
    saas_app_version.save()
    saas_app.current_version = saas_app_version
    saas_app.save()
    return saas_app_version.id


def get_saas_settings_for_deploy(saas_app_version):
    """
    for deploy
    """
    try:
        settings = json.loads(saas_app_version.settings)
    except:
        settings = {}
        logger.exception(u'应用配置信息解析异常')

    upload_file = saas_app_version.upload_file
    saas_settings = {}
    saas_settings.update(settings)
    saas_settings['url'] = upload_file.url
    saas_settings['md5'] = upload_file.md5
    return saas_settings


def _get_app_info_from_settings(app_code, saas_app_version):
    """
    从配置中解析得到app_info
    """
    settings = saas_app_version.get_settings()
    if not settings:
        msg = u'应用(code:%s)配置信息解析异常' % app_code
        return (
         False, msg, None)
    introduction = settings.get('introduction', '')
    language = settings.get('language', 'python')
    is_use_celery = settings.get('is_use_celery', False)
    author = settings.get('author', '')
    category = settings.get('category', u'其它')
    desktop = settings.get('desktop') or {}
    app_info = {'introduction': introduction,
       'language': language,
       'author': author,
       'is_use_celery': is_use_celery,
       'is_use_celery_beat': is_use_celery,
       'category': category,
       'desktop': desktop
       }
    return (
     True, 'success', app_info)


def create_app(saas_app_version):
    """
    新建一个app, 创建数据库, 并将其关联到SaaSApp
    
    关系1: SaaSApp -> SaaSAppVersion -> SaaSUploadFile
    关系2: SaaSApp -> App
    """
    saas_app = saas_app_version.saas_app
    app_code = saas_app.code
    app_name = saas_app.name
    is_success, msg, app_info = _get_app_info_from_settings(app_code, saas_app_version)
    if not is_success:
        return (False, msg, None)
    res, msg, app = save_app_info(app_code, app_name, is_create=True, **app_info)
    if not res:
        return (False, msg, None)
    res, msg = create_saas_app_db(app_code)
    if not res:
        App.objects.filter(code=app_code).delete()
        return (
         False, msg, None)
    saas_app.current_version = saas_app_version
    saas_app.save()
    return (
     True, 'success', app)


def update_app(saas_app_version):
    """
    更新app
    language / is_use_celery / language / version等字段
    """
    saas_app = saas_app_version.saas_app
    app_code = saas_app.code
    is_success, msg, app_info = _get_app_info_from_settings(app_code, saas_app_version)
    if not is_success:
        return (False, msg)
    res, msg, app = save_app_info(saas_app.code, saas_app.name, is_create=False, **app_info)
    if not res:
        return (res, msg)
    saas_app.current_version = saas_app_version
    saas_app.save()
    return (
     True, 'success')


def save_app_info(code, name, is_create=True, **app_info):
    """
    将app的基本信息存入到 paas_app 表中
    
    如：code、name、introduction 等
    code 重复 则直接返回异常由上层处理
    name 重复 则直接保存code信息
    """
    if not code:
        return (False, u'缺少参数：code', None)
    language = app_info.get('language', 'python')
    if is_create:
        is_success, register_result = register_app(code, name, language)
        if register_result:
            token = register_result.get('token', '') if 1 else ''
            error_msg = is_success and token or u'应用[%s]注册失败, 失败信息:%s. 可能原因:App Engine 未正常启动' % (code, register_result)
            logger.error(error_msg)
            return (
             False, error_msg, None)
    with transaction.atomic():
        if is_create:
            try:
                app = App.objects.create(code=code)
                app.auth_token = token
            except:
                logger.exception(u'SaaS 应用code(%s)已经存在app表中' % code)
                return (
                 False, u'SaaS 应用ID[%s]与已有 PaaS 应用ID冲突' % code, None)

        else:
            app, _c = App.objects.get_or_create(code=code)
        try:
            old_name = app.name
            is_name_exist = App.objects.filter(name=name).exclude(name=old_name).exists()
            app.name = code if is_name_exist or not name else name
            app.save()
        except:
            logger.exception(u'SaaS 应用name(%s)已经存在app表中' % name)
            return (
             False, u'SaaS 应用名称[%s]与已有应用名称冲突' % name, None)

        app.is_saas = True
        app.language = language
        app.introduction = app_info.get('introduction')
        app.is_use_celery = app_info.get('is_use_celery')
        app.is_use_celery_beat = app_info.get('is_use_celery_beat')
        app.creater = app_info.get('author')
        app.save()
        paas_db = settings.DATABASES.get('default', {})
        app_sec_info, _c = SecureInfo.objects.get_or_create(app_code=code)
        app_sec_info.db_host = paas_db.get('HOST')
        app_sec_info.db_username = paas_db.get('USER')
        app_sec_info.db_password = paas_db.get('PASSWORD')
        app_sec_info.db_name = code
        app_sec_info.save()
    return (True, '', app)


def upload_response_tpl(result, message, data={}):
    """
    将上传文件的结果通过js的方法返回渲染
    """
    if result:
        span_type = 'success' if 1 else 'danger'
        fa_type = 'icon-check1' if result else 'icon-exclamation-triangle'
        saas_app_version_id = data.get('saas_app_version_id', '')
        file_version_display = data.get('file_version_display', '')
        if_success_do_remove_js = 'window.parent.document.getElementById("saas_app_online").removeAttribute("disabled");'
        if_success_do_remove_js = result or ''
    response_tpl = UPLOAD_RESPONSE_FORMAT.format(span_type=span_type, message=message, fa_type=fa_type, saas_app_version_id=saas_app_version_id, file_version_display=file_version_display, if_success_do_remove_js=if_success_do_remove_js)
    response = HttpResponse()
    response['Content-Type'] = 'text/html'
    response['charset'] = 'utf-8'
    response.write(response_tpl)
    return response


def create_saas_app_db(app_code):
    """
    给SaaS应用创建db信息
    
    db的用户信息跟平台保持一致
    """
    app_sec_info, _c = SecureInfo.objects.get_or_create(app_code=app_code)
    db_name = app_sec_info.db_name
    create_sql = CREATE_PAAS_DB_SQL % db_name
    res, msg = execute_sql('default', create_sql)
    if not res:
        base_url = ('{}/saas/info/{}').format(settings.SITE_URL, app_code)
        msg = (u"\u521b\u5efa\u5e94\u7528db\u5931\u8d25\uff0c\u8bf7\u624b\u52a8\u521b\u5efadb\u540e\uff0c\u70b9\u51fb <a href='{}'>\u8fd9\u91cc</a> \u4fee\u6539\u914d\u7f6e\u4fe1\u606f\u540e\u91cd\u65b0\u90e8\u7f72").format(base_url)
    return (res, msg)


def execute_sql(db_alias, operation, params=None):
    """
    @summary: 查询数据库中的数据
    @param db_alias: 数据连接别名，数据连接由settings.DATABASES设置
    @param execute_sql: 执行更新操作的SQL语句
    @param params: SQL语句中条件参数
    @return: 返回是否执行成功
    """
    cursor = connections[db_alias].cursor()
    res = True
    msg = ''
    try:
        cursor.execute(operation)
        msg = u'执行成功'
    except:
        logger.exception(u'sql语句执行失败')
        res = False
    finally:
        cursor.close()

    return (res, msg)


def update_online_version(app_code):
    """
    更新 在线版本 为 处理前版本
    """
    logger.info(u'更新SaaSApp的online_version: %s' % app_code)
    try:
        app = App.objects.get(code=app_code)
        saas_app = SaaSApp.objects.get(app=app)
        saas_app.online_version = saas_app.current_version
        saas_app.save()
    except:
        logger.exception(u'更新SaaSApp的online_version失败. app_code=%s' % app_code)
        return False

    return True


@transaction.atomic
def delete_saas_app(app_code, username):
    """
    清除应用
    """
    saas_app = SaaSApp.objects.get(code=app_code)
    if saas_app.state not in (1, ):
        return (False, u'应用已经部署过, 无法进行删除操作')
    saas_app.app = None
    saas_app.current_version = None
    saas_app.online_version = None
    saas_app.save()
    try:
        SecureInfo.objects.filter(app_code=app_code).delete()
        App.objects.filter(code=app_code).delete()
        Record.objects.filter(app_code=app_code).update(version='last')
        logger.info(u'[app:%s] 删除成功' % app_code)
    except:
        msg = u'应用删除失败！'
        logger.exception(u'[app:%s] %s' % (app_code, msg))
        return (
         False, msg)

    try:
        versions = SaaSAppVersion.objects.filter(saas_app=saas_app).all()
        for version in versions:
            upload_file = version.upload_file
            upload_file.file.delete()
            upload_file.delete()
            version.delete()

        saas_app.delete()
    except:
        message = u'删除 SaaSApp 相关数据失败, app_code=%s' % app_code
        logger.exception(message)
        return (
         False, message)

    extra_data = {'username': username}
    record_user_operate(app_code=app_code, username=username, operate_type=UserOperateTypeEnum.APP_DELETE, extra_data=extra_data)
    return (
     True, u'应用删除成功')


def get_app_cur_version_info(saas_app, is_offline=False):
    """
    获取应用的版本信息
    
    下架（is_offline＝True）则获取 online_version 信息
    其他：先获取 online_versions 信息，无则获取 current_version
    """
    app_version = saas_app.online_version
    if not app_version and not is_offline:
        app_version = saas_app.current_version
    if not app_version:
        return (False, {})
    try:
        settings = json.loads(app_version.settings)
        introduction = settings.get('introduction', '--')
        settings = json.dumps(settings, indent=4, ensure_ascii=False)
    except:
        settings = '--'
        introduction = '--'
        logger.exception(u'解析应用(%s)的配置信息出错' % saas_app.code)

    app_file = app_version.upload_file if app_version else None
    file_size = app_file.size if app_file else ''
    try:
        if file_size:
            file_size = round(file_size / 1024 / 1024.0, 2)
    except TypeError:
        logger.exception(u'文件大小转换出错')

    file_name = app_file.name if app_file else ''
    version = app_version.version
    file_version_display = '%s (V%s)' % (file_name, version)
    version_info = {'id': app_version.id,
       'version': version,
       'settings': settings,
       'introduction': introduction,
       'file_name': file_name,
       'file_version_display': file_version_display,
       'file_size': file_size,
       'md5': app_file.md5 if app_file else ''
       }
    return (
     True, version_info)


def is_saas_app_already_exist(app_code):
    """
    判断app_code是否已存在
    """
    return SaaSApp.objects.filter(code=app_code).exists()
